﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;
using Ionic.Zip;
using JsonFx.BuildTools.CssCompactor;

namespace EpubSqueaker
{
	public class XhtmlSplitter
	{
		private static bool? preserveWhitespace = false;

		private ZipFile epub;
		private Dictionary<string, List<string>> allPageBreakBeforeClasses, allPageBreakAfterClasses;

		public XhtmlSplitter(string epubFileName)
		{
			epub = ZipFile.Read(epubFileName);
			allPageBreakBeforeClasses = new Dictionary<string, List<string>>();
			allPageBreakAfterClasses = new Dictionary<string, List<string>>();
		}

		public XhtmlSplitter(ZipFile epubFile)
		{
			epub = epubFile;
			allPageBreakBeforeClasses = new Dictionary<string, List<string>>();
			allPageBreakAfterClasses = new Dictionary<string, List<string>>();
		}

		/// <summary>
		/// Split an XHTML file at its page-break-before/page-break-after classes.
		/// </summary>
		/// <param name="epub">ePub to split the XHTML files in.</param>
		public bool SplitXhtmlFiles()
		{
			if (epub == null)
				throw new InvalidOperationException("epub file could not be processed!");

			bool returnVal = false;

			// Theoretically at this point we know where the OPF file is. Let's open 'er up!
			foreach (ZipEntry zeOpf in EpubTools.OpfFilesInEpub(epub))
			{
				XmlDocument opf = CreateXmlDocumentFromZipEntry(zeOpf);

				var opfFile = new FileReference(zeOpf.FileName);
				FindAllPageBreakClasses(opfFile.DirectoryPathFromOpf, opf);

				// Set up our OPF nodes to delete later - can't remove them in the
				// middle of an iteration.
				var nodesToDelete = new List<XmlNode>();

				// Set up our XML namespace information.
				XmlNamespaceManager nsmgrOpf = new XmlNamespaceManager(opf.NameTable);
				nsmgrOpf.AddNamespace("opf", "http://www.idpf.org/2007/opf");

				var listValidMediaTypes = new List<string>() { "application/xhtml+xml", "application/x-dtbook+xml", "text/x-oeb1-document" };
				var manifest = opf.SelectSingleNode("//opf:package/opf:manifest", nsmgrOpf);
				foreach (XmlNode item in manifest.SelectNodes("//opf:item", nsmgrOpf))
				{
					if (!listValidMediaTypes.Contains(item.Attributes.GetNamedItem("media-type").Value))
					{
						continue;
					}
					else if (item.Attributes.GetNamedItem("id").Value.StartsWith("body", StringComparison.Ordinal) &&
						item.Attributes.GetNamedItem("href").Value.StartsWith("body", StringComparison.Ordinal))
					{
						// This is probably one of our magic body files - ignore it.
						nodesToDelete.Add(item);
						continue;
					}

					var htmlFile = new FileReference(opfFile.DirectoryPathFromOpf, item.Attributes.GetNamedItem("href").Value);

					foreach (ZipEntry zeDocument in epub.SelectEntries(htmlFile.FileNameForZip, htmlFile.DirectoryName))
					{
						var xmlDocument = CreateXmlDocumentFromZipEntry(zeDocument);
						XmlNamespaceManager nsmgrDoc = new XmlNamespaceManager(xmlDocument.NameTable);
						nsmgrDoc.AddNamespace("xhtml", "http://www.w3.org/1999/xhtml");
						XmlNode xmlBody = xmlDocument.SelectSingleNode("//xhtml:html/xhtml:body", nsmgrDoc);

						// Go through the spine and find items that we think match B&N's code.
						var spine = opf.SelectSingleNode("//opf:package/opf:spine", nsmgrOpf);
						foreach (XmlNode itemref in spine.SelectNodes("//opf:itemref[starts-with(@idref, \"body\")]", nsmgrOpf))
						{
							var itemBody = opf.SelectSingleNode("//opf:package/opf:manifest/opf:item[@id=\"" + itemref.Attributes.GetNamedItem("idref").Value + "\"]", nsmgrOpf);
							if (itemBody.Attributes.GetNamedItem("media-type").Value == "application/xhtml+xml")
							{
								foreach (ZipEntry zeDocumentSegment in epub.SelectEntries(itemBody.Attributes.GetNamedItem("href").Value, opfFile.DirectoryPathFromOpf))
								{
									var xmlBodySegment = CreateXmlDocumentFromZipEntry(zeDocumentSegment);
									XmlNamespaceManager nsmgrBodySegment = new XmlNamespaceManager(xmlBodySegment.NameTable);
									nsmgrBodySegment.AddNamespace("xhtml", "http://www.w3.org/1999/xhtml");

									XmlNode bodyNewChild = xmlBodySegment.SelectSingleNode("//xhtml:html/xhtml:body", nsmgrBodySegment).FirstChild;
									while (bodyNewChild != null)
									{
										xmlBody.AppendChild(xmlDocument.ImportNode(bodyNewChild, true));
										bodyNewChild = bodyNewChild.NextSibling;
									}

									epub.RemoveEntry(zeDocumentSegment.FileName);
								}

								nodesToDelete.Add(itemref);
							}
						}

						var splitFiles = SplitXhtml(xmlDocument);
						if (splitFiles.Count > 1)
						{
							returnVal = true;

							// Save the files into the ZIP, and add them to the manifest.
							var itemToInsertAfter = item;
							for (int count = 0; count < splitFiles.Count; count ++ )
							{
								var itemToInsert = item.CloneNode(true);

								var newHref = BuildNewHref(htmlFile, count);
								itemToInsert.Attributes.GetNamedItem("href").Value = newHref.FullPathFromOpf;
								itemToInsert.Attributes.GetNamedItem("id").Value += "_" + System.String.Format(CultureInfo.InvariantCulture, "_{0}", count);

								using (var memoryStream = new MemoryStream())
								{
									splitFiles[count].Save(memoryStream);
									memoryStream.Seek(0, SeekOrigin.Begin);

									// newHref is relative to the OPF path; we need to concatenate
									// the two when actually adding it to the ZIP file.
									epub.AddEntry(newHref.FullPath, memoryStream);
									epub.Save();
								}

								manifest.InsertAfter(itemToInsert, itemToInsertAfter);
								itemToInsertAfter = itemToInsert;
							}

							// Remove the original file from the manifest.
							nodesToDelete.Add(item);
							epub.RemoveEntry(opfFile.DirectoryPathFromOpf + item.Attributes.GetNamedItem("href").Value);

							// Replace the original item in the spine with these.
							foreach (XmlNode itemref in spine.SelectNodes("//opf:itemref[@idref=\"" + item.Attributes.GetNamedItem("id").Value + "\"]", nsmgrOpf))
							{
								var itemrefLast = itemref;
								for (int count = 0; count < splitFiles.Count; count ++ )
								{
									var itemrefNew = itemref.CloneNode(true);
									itemrefNew.Attributes.GetNamedItem("idref").Value += "_" + System.String.Format(CultureInfo.InvariantCulture, "_{0}", count);

									spine.InsertAfter(itemrefNew, itemrefLast);
									itemrefLast = itemrefNew;
								}
								nodesToDelete.Add(itemref);
							}

							#region Update the TOC.
							var tocAttribute = spine.Attributes.GetNamedItem("toc");
							if (tocAttribute != null)
							{
								var tocFile = new FileReference(opfFile.DirectoryPathFromOpf, manifest.SelectSingleNode("//opf:item[@id=\"" + tocAttribute.Value + "\"]", nsmgrOpf).Attributes.GetNamedItem("href").Value);
								foreach (ZipEntry zeToc in epub.SelectEntries(tocFile.FileNameForZip, tocFile.DirectoryName))
								{
									var toc = CreateXmlDocumentFromZipEntry(zeToc);

									XmlNamespaceManager nsmgrToc = new XmlNamespaceManager(toc.NameTable);
									nsmgrToc.AddNamespace("ncx", "http://www.daisy.org/z3986/2005/ncx/");

									foreach (XmlNode content in toc.SelectNodes("//ncx:ncx/ncx:navMap/ncx:navPoint/ncx:content[starts-with(@src, \"" + htmlFile.FullPathFromOpf + "\") or starts-with(@src, \"body\")]", nsmgrToc))
									{
										var newHrefBuilder = new StringBuilder();
										newHrefBuilder.Append(htmlFile.DirectoryPathFromOpf);
										newHrefBuilder.Append(htmlFile.FileNameWithoutExtension);

										var hrefPieces = content.Attributes.GetNamedItem("src").Value.Split(new char[] { '#' }, 2, StringSplitOptions.None);
										if (hrefPieces.Length == 1)
										{
											// This only points to the original file. Extend it out with an _0.
											newHrefBuilder.Append("_0"); //System.String.Format(CultureInfo.InvariantCulture, "_{0}", count));
											newHrefBuilder.Append(htmlFile.Extension);
										}
										else
										{
											bool appended = false;
											for (int count = 0; count < splitFiles.Count; count++)
											{
												var splitFile = splitFiles[count];
												if (splitFile.GetElementById(hrefPieces[1]) != null)
												{
													appended = true;
													newHrefBuilder.Append(System.String.Format(CultureInfo.InvariantCulture, "_{0}", count));
													newHrefBuilder.Append(htmlFile.Extension);
													newHrefBuilder.Append("#");
													newHrefBuilder.Append(hrefPieces[1]);
													break;
												}
											}

											if (!appended)
												throw new InvalidDataException(String.Format(CultureInfo.CurrentCulture, "Could not find anchor {0} in file {1}", hrefPieces[1], hrefPieces[0]));
										}
										content.Attributes.GetNamedItem("src").Value = newHrefBuilder.ToString();
									}

									using (var memoryStream = new MemoryStream())
									{
										toc.Save(memoryStream);
										memoryStream.Seek(0, SeekOrigin.Begin);
										epub.UpdateEntry(zeToc.FileName, memoryStream);
										epub.Save();
									}
								}
							}
							#endregion
						}
					}
				}

				foreach (var nodeToDelete in nodesToDelete)
				{
					nodeToDelete.ParentNode.RemoveChild(nodeToDelete);
				}

				if (returnVal)
				{
					/* Technically this could be invoked if the epub has two OPFs and only
					 * the first is changed. But a file should never *have* more than one
					 * OPF, so it's functionally moot. */

					// Save the OPF to a stream, and write the stream to the epub.
					using (var opfStream = new MemoryStream())
					{
						opf.Save(opfStream);
						opfStream.Seek(0, SeekOrigin.Begin);

						// Doing a straight update results in a corrupt file -
						// must be a bug in DotNetZip. Need to remove, save,
						// then add.
						epub.RemoveEntry(zeOpf.FileName);
						epub.Save();
						epub.AddEntry(zeOpf.FileName, opfStream);
						epub.Save();
					}
				}
			}

			return returnVal;
		}

		/// <summary>
		/// Combine an old file reference and the split file count to create a new file reference.
		/// </summary>
		/// <param name="oldHref">Original FileReference.</param>
		/// <param name="count">Count of the split file.</param>
		private static FileReference BuildNewHref(FileReference oldHref, int count)
		{
			var newHrefBuilder = new StringBuilder();
			newHrefBuilder.Append(oldHref.DirectoryPathFromOpf);
			newHrefBuilder.Append(oldHref.FileNameWithoutExtension);
			newHrefBuilder.Append(System.String.Format(CultureInfo.InvariantCulture, "_{0}", count));
			newHrefBuilder.Append(oldHref.Extension);

			return new FileReference(oldHref.OpfPath, newHrefBuilder.ToString());
		}

		/// <summary>
		/// Fill the dictionaries with all page-break classes in CSS files referenced by the OPF.
		/// </summary>
		/// <param name="opfRoot">Root path of the OPF file. Must contain the trailing slash.</param>
		/// <param name="opf">XmlDocument of the OPF file.</param>
		private void FindAllPageBreakClasses(string opfRoot, XmlDocument opf)
		{
			if (String.IsNullOrEmpty(opfRoot))
				opfRoot = "";
			else if (!opfRoot.EndsWith("/", StringComparison.OrdinalIgnoreCase))
				throw new ArgumentException("Non-blank OPF path must end in a slash!", "opfRoot");

			// Set up our XML namespace information.
			XmlNamespaceManager nsmgr = new XmlNamespaceManager(opf.NameTable);
			nsmgr.AddNamespace("opf", "http://www.idpf.org/2007/opf");

			/* When we extract the CSS, we don't want to maintain the directory
			 * structure. This is the only raw file extraction we do, so we
			 * don't want to affect the file globally; instead, save it so we
			 * can restore it afterwards. */
			var flattenFoldersOnExtract = epub.FlattenFoldersOnExtract;
			epub.FlattenFoldersOnExtract = true;

			// Parse the OPF for all CSS files, and 
			foreach (XmlNode item in opf.SelectNodes("//opf:package/opf:manifest/opf:item[@media-type=\"text/css\"]", nsmgr))
			{
				FileReference cssFile = new FileReference(opfRoot, item.Attributes["href"].Value);

				foreach (ZipEntry zeCss in epub.SelectEntries(cssFile.FileNameForZip, cssFile.DirectoryName))
				{
					// Extract the CSS to the temp folder.
					zeCss.Extract(System.IO.Path.GetTempPath(), ExtractExistingFileAction.OverwriteSilently);

					// Parse the CSS file.
					List<String> pageBreakBeforeClasses, pageBreakAfterClasses;
					string cssPathTemp = System.IO.Path.GetTempPath() + cssFile.FileName;
					FindPageBreakClasses(cssPathTemp, out pageBreakBeforeClasses, out pageBreakAfterClasses);

					allPageBreakBeforeClasses.Add(cssFile.FullPathFromOpf, pageBreakBeforeClasses);
					allPageBreakAfterClasses.Add(cssFile.FullPathFromOpf, pageBreakAfterClasses);

					// Clean up the temp folder.
					System.IO.File.Delete(cssPathTemp);
				}
			}

			epub.FlattenFoldersOnExtract = flattenFoldersOnExtract;
		}

		/// <summary>
		/// Build lists of all page break classes in a CSS file.
		/// </summary>
		/// <param name="cssPathTemp">File system path to a CSS file.</param>
		/// <param name="pageBreakBeforeClasses">All classes specifying a breaking page-break-before.</param>
		/// <param name="pageBreakAfterClasses">All classes specifying a breaking page-break-after.</param>
		private static void FindPageBreakClasses(string cssPathTemp, out List<string> pageBreakBeforeClasses, out List<string> pageBreakAfterClasses)
		{
			pageBreakBeforeClasses = new List<string>();
			pageBreakAfterClasses = new List<string>();

			var parser = new JsonFx.BuildTools.CssCompactor.CssParser(cssPathTemp);

			// Parse the CSS file.
			foreach (CssStatement cssStatement in parser.StyleSheet.Statements)
			{
				if (cssStatement.GetType() == typeof(CssRuleSet))
				{
					var cssRuleSet = (CssRuleSet)cssStatement;
					foreach (CssDeclaration cssDeclaration in cssRuleSet.Declarations)
					{
						if (cssDeclaration.Property.Equals("page-break-before", StringComparison.OrdinalIgnoreCase))
						{
							checkForBreaks(cssRuleSet, cssDeclaration, ref pageBreakBeforeClasses);
						}
						else if (cssDeclaration.Property.Equals("page-break-after", StringComparison.OrdinalIgnoreCase))
						{
							checkForBreaks(cssRuleSet, cssDeclaration, ref pageBreakAfterClasses);
						}
					}
				}
			}
		}

		/// <summary>
		/// Unzip a ZipEntry and build an XmlDocument from it.
		/// </summary>
		/// <param name="toUnzip">ZipEntry to unzip.</param>
		private static XmlDocument CreateXmlDocumentFromZipEntry(ZipEntry toUnzip)
		{
			return CreateXmlDocumentFromZipEntry(toUnzip, true, new DtdResolver());
		}

		/// <summary>
		/// Unzip a ZipEntry and build an XmlDocument from it.
		/// </summary>
		/// <param name="toUnzip">ZipEntry to unzip.</param>
		/// <param name="convertXml11ToXml10">If XML 1.1 should be pseudoconverted to XML 1.0.</param>
		/// <param name="xmlResolver">Override default XmlDocument.XmlResolver. Null will be treated as the default resolver.</param>
		private static XmlDocument CreateXmlDocumentFromZipEntry(ZipEntry toUnzip, bool convertXml11ToXml10, XmlResolver xmlResolver)
		{
			XmlDocument toReturn = new XmlDocument();

			using (var streamXml = new MemoryStream())
			{
				// Load the raw XML into memory.
				toUnzip.Extract(streamXml);
				streamXml.Seek(0, SeekOrigin.Begin);

				// HACK: Convert from XML 1.1 to XML 1.0 if necessary, since Microsoft's XML tools only support 1.0.
				if (convertXml11ToXml10 && XmlTools.ConvertXml11ToXml10(streamXml))
				{
					System.Diagnostics.Debug.WriteLine("Updated OPF " + toUnzip.FileName);
				}

				if (preserveWhitespace.HasValue)
					toReturn.PreserveWhitespace = preserveWhitespace.Value;
				if (xmlResolver != null)
					toReturn.XmlResolver = xmlResolver;
				toReturn.Load(streamXml);
			}

			return toReturn;
		}

		/// <summary>
		/// Look for CSS types indicating a page break, and if found add these classes
		/// to the array.
		/// </summary>
		/// <param name="cssRuleSet">CSS rule we are parsing.</param>
		/// <param name="cssDeclaration">CSS declaration within the rule to parse.</param>
		/// <param name="pageBreakClasses">List of classes bearing this page break type.</param>
		private static void checkForBreaks(CssRuleSet cssRuleSet, CssDeclaration cssDeclaration, ref List<String> pageBreakClasses)
		{
			foreach (CssString cssValue in cssDeclaration.Value.Values)
			{
				if (cssValue.Value.Equals("always", StringComparison.OrdinalIgnoreCase) ||
					cssValue.Value.Equals("left", StringComparison.OrdinalIgnoreCase) ||
					cssValue.Value.Equals("right", StringComparison.OrdinalIgnoreCase))
				{
					foreach (var cssSelector in cssRuleSet.Selectors)
					{
						if (cssSelector.Value.StartsWith(".", StringComparison.Ordinal))
						{
							pageBreakClasses.Add(cssSelector.Value.Substring(1));
						}
					}
				}
			}
		}

		/// <summary>
		/// Split the specified XmlDocument. Requires the page-break dictionaries to have been built.
		/// </summary>
		/// <param name="input">Text of the HTML to split.</param>
		/// <param name="xml10Path">Path to the XML 1.0 file.</param>
		private List<XmlDocument> SplitXhtml(XmlDocument htmlInput)
		{
			if (htmlInput == null)
			{
				throw new ArgumentNullException("htmlInput");
			}
			
			List<string> pageBreakBeforeClasses, pageBreakAfterClasses;
			SelectCssClasses(htmlInput, out pageBreakBeforeClasses, out pageBreakAfterClasses);

			var toReturn = new List<XmlDocument>();

			XmlNamespaceManager nsmgrInput = new XmlNamespaceManager(htmlInput.NameTable);
			nsmgrInput.AddNamespace("xhtml", "http://www.w3.org/1999/xhtml");

			XmlNode htmlInputRoot = htmlInput.SelectSingleNode("//xhtml:html", nsmgrInput);
			XmlNode htmlInputBody = htmlInput.SelectSingleNode("//xhtml:html/xhtml:body", nsmgrInput);

			var htmlInputBodyPrev = htmlInputRoot.CloneNode(true).SelectSingleNode("//xhtml:body", nsmgrInput);
			var htmlInputBodyNext = htmlInputRoot.CloneNode(true).SelectSingleNode("//xhtml:body", nsmgrInput);

			XmlDocument documentForSave = new XmlDocument();
			XmlNamespaceManager nsmgr;

			bool isContaminated = ProcessDocument(htmlInputBody, ref htmlInputBodyPrev, ref htmlInputBodyNext, pageBreakBeforeClasses, pageBreakAfterClasses);
			while (isContaminated)
			{
				documentForSave = CloneDocument(htmlInput);
				nsmgr = new XmlNamespaceManager(documentForSave.NameTable);
				nsmgr.AddNamespace("xhtml", "http://www.w3.org/1999/xhtml");
				documentForSave.DocumentElement.ReplaceChild(documentForSave.ImportNode(htmlInputBodyPrev, true), documentForSave.SelectSingleNode("//xhtml:html/xhtml:body", nsmgr));
				toReturn.Add(documentForSave);

				var htmlInputBodyPass = htmlInputBodyNext;
				htmlInputBodyPrev = htmlInputBodyNext.CloneNode(true);
				htmlInputBodyNext = htmlInputBodyNext.CloneNode(true);

				isContaminated = ProcessDocument(htmlInputBodyPass, ref htmlInputBodyPrev, ref htmlInputBodyNext, pageBreakBeforeClasses, pageBreakAfterClasses);
			}

			// Finally, write out our last file.
			documentForSave = CloneDocument(htmlInput);
			nsmgr = new XmlNamespaceManager(documentForSave.NameTable);
			nsmgr.AddNamespace("xhtml", "http://www.w3.org/1999/xhtml");
			documentForSave.DocumentElement.ReplaceChild(documentForSave.ImportNode(htmlInputBodyPrev, true), documentForSave.SelectSingleNode("//xhtml:html/xhtml:body", nsmgr));
			toReturn.Add(documentForSave);

			return toReturn;
		}

		private void SelectCssClasses(XmlDocument html, out List<string> pageBreakBeforeClasses, out List<string> pageBreakAfterClasses)
		{
			pageBreakBeforeClasses = new List<string>();
			pageBreakAfterClasses = new List<string>();

			XmlNamespaceManager nsmgr = new XmlNamespaceManager(html.NameTable);
			nsmgr.AddNamespace("xhtml", "http://www.w3.org/1999/xhtml");

			foreach (XmlElement stylesheet in html.SelectNodes("//xhtml:html/xhtml:head/xhtml:link[@rel=\"stylesheet\"]", nsmgr))
			{
				if (stylesheet.HasAttribute("href"))
				{
					pageBreakBeforeClasses.AddRange(allPageBreakBeforeClasses[stylesheet.Attributes["href"].Value]);
					pageBreakAfterClasses.AddRange(allPageBreakAfterClasses[stylesheet.Attributes["href"].Value]);
				}
			}
		}

		/// <summary>
		/// Clone a given XmlDocument using a <see cref="DtdResolver" />.
		/// </summary>
		/// <param name="toClone">Document to clone.</param>
		private static XmlDocument CloneDocument(XmlDocument toClone)
		{
			return CloneDocument(toClone, new DtdResolver());
		}

		/// <summary>
		/// Clone a given XmlDocument using a given non-default XmlResolver.
		/// </summary>
		/// <param name="toClone">Document to clone.</param>
		/// <param name="toResolve">Resolver to use.</param>
		private static XmlDocument CloneDocument(XmlDocument toClone, XmlResolver toResolve)
		{
			XmlDocument toReturn = (XmlDocument)toClone.CloneNode(false);
			toReturn.XmlResolver = toResolve;
			foreach (XmlNode toCloneChild in toClone.ChildNodes)
			{
				toReturn.AppendChild(toReturn.ImportNode(toCloneChild, true));
			}
			return toReturn;
		}

		/// <summary>
		/// Split the HTML document tree into pre- and post-break trees. Return if we actually performed a split.
		/// </summary>
		/// <param name="htmlDocument">Original HTML tree.</param>
		/// <param name="htmlDocumentPrev">Cloned tree that will be the pre-break HTML.</param>
		/// <param name="htmlDocumentNext">Cloned tree that will be the post-break HTML.</param>
		/// <param name="pageBreakClasses">Classes to create a page break on.</param>
		private static bool ProcessDocument(XmlNode htmlDocument, ref XmlNode htmlDocumentPrev, ref XmlNode htmlDocumentNext, List<string> pageBreakBeforeClasses, List<string> pageBreakAfterClasses)
		{
			bool foundYet = false;
			ProcessDocument(htmlDocument, ref htmlDocumentPrev, ref htmlDocumentNext, ref foundYet, pageBreakBeforeClasses, pageBreakAfterClasses);
			return foundYet;
		}

		/// <summary>
		/// Split the HTML document tree into pre- and post-break trees. Return if we actually performed a split.
		/// </summary>
		/// <param name="htmlDocument">Original HTML tree.</param>
		/// <param name="htmlDocumentPrev">Cloned tree that will be the pre-break HTML.</param>
		/// <param name="htmlDocumentNext">Cloned tree that will be the post-break HTML.</param>
		/// <param name="foundYet">Used to track if we've found our split for the current document set.</param>
		/// <param name="pageBreakClasses">Classes to create a page break on.</param>
		private static void ProcessDocument(XmlNode htmlDocument, ref XmlNode htmlDocumentPrev, ref XmlNode htmlDocumentNext, ref bool foundYet, List<string> pageBreakBeforeClasses, List<string> pageBreakAfterClasses)
		{
			// If htmlDocument has no children, we can't recurse futher; return out now.
			if (!htmlDocument.HasChildNodes)
			{
				return;
			}

			XmlNode childPrev = null;
			XmlNode childNext = null;

			foreach (XmlNode child in htmlDocument.ChildNodes)
			{
				var foundThisIteration = false;

				childPrev = (childPrev == null) ? htmlDocumentPrev.FirstChild : childPrev.NextSibling;
				childNext = (childNext == null) ? htmlDocumentNext.FirstChild : childNext.NextSibling;

				if (!foundYet)
				{
					if (child.Attributes != null)
					{
						foreach (XmlAttribute htmlAttribute in child.Attributes)
						{
							if (htmlAttribute.Name == "class")
							{
								var classList = (IList<string>)htmlAttribute.Value.Split(' ');
								foreach (var pageBreakClass in pageBreakBeforeClasses)
								{
									if (classList.Contains(pageBreakClass))
									{
										foundYet = true;
										foundThisIteration = true;

										childPrev = childPrev.PreviousSibling;
										htmlDocumentPrev.RemoveChild((childPrev == null) ? htmlDocumentPrev.FirstChild : childPrev.NextSibling);
									}
								}

								// TODO: Handle pageBreakAfterClasses.
							}
						}
					}

					if (child.HasChildNodes && (childPrev != null) && (childNext != null))
					{
						XmlNode subChildPrev = null;
						XmlNode subChildNext = null;

						foreach (XmlNode htmlChild in child.ChildNodes)
						{
							subChildPrev = (subChildPrev == null) ? childPrev.FirstChild : subChildPrev.NextSibling;
							subChildNext = (subChildNext == null) ? childNext.FirstChild : subChildNext.NextSibling;
							ProcessDocument(htmlChild, ref subChildPrev, ref subChildNext, ref foundYet, pageBreakBeforeClasses, pageBreakAfterClasses);
						}
					}

					Debug.Assert(childNext != null, "childNext is null - this is probably not good");
					if ((!foundYet) || foundThisIteration)
					{
						// If we haven't found the child yet, or found it on this pass,
						// whack the node on the next tree.
						childNext = childNext.PreviousSibling;
						htmlDocumentNext.RemoveChild((childNext == null) ? htmlDocumentNext.FirstChild : childNext.NextSibling);
					}
				}
				else
				{
					// We found the child already, whack the node on the previous tree.
					childPrev = childPrev.PreviousSibling;
					htmlDocumentPrev.RemoveChild((childPrev == null) ? htmlDocumentPrev.FirstChild : childPrev.NextSibling);
				}
			}

			if (!htmlDocumentNext.HasChildNodes && (htmlDocumentNext.ParentNode != null))
			{
				var temp = htmlDocumentNext.PreviousSibling;
				htmlDocumentNext.ParentNode.RemoveChild(htmlDocumentNext);
				htmlDocumentNext = temp;
			}
			if (!htmlDocumentPrev.HasChildNodes && (htmlDocumentPrev.ParentNode != null))
			{
				var temp = htmlDocumentPrev.PreviousSibling;
				htmlDocumentPrev.ParentNode.RemoveChild(htmlDocumentPrev);
				htmlDocumentPrev = temp;
			}
		 }
	}
}
